﻿using System;
using System.Text;
using System.Diagnostics;
using System.Linq;
using System.IO;
using System.Collections;
using System.Globalization;
using System.Collections.Generic;
using System.Runtime.Serialization;

using StringBuilder = System.Text.StringBuilder;
using BindingFlags = System.Reflection.BindingFlags;
using CallingConventions = System.Reflection.CallingConventions;

namespace Sencha.Serialization
{
	using Contracts;
	using Serializers;

	// aspect: skip undefined values on serialization for more compact output
	public sealed class JsonSerializer : XmlObjectSerializer
	{
		private DataContract m_contract;

		private Type m_rootType;
		private string[] m_dateFormats;
		private Features m_features;
		private IFormatProvider m_format;
		private Encoding m_encoding;

		/// <summary>
		/// Ignore custom serialization/deserialization logic through IJsonSerializable. Default is false.
		/// </summary>
		/// <see cref="Sencha.Serialization.IJsonSerializable"/>
		public bool IgnoreCustomSerialization { get { return (m_features & Features.IgnoreCustomSerialization) != 0; } set { m_features = value ? m_features | Features.IgnoreCustomSerialization : m_features & ~Features.IgnoreCustomSerialization; } }
		/// <summary>
		/// Allow thread single object as array when deserializing it.
		/// False if the array was expected but received only single value (not in the array), will be thrown an exception. Default is true
		/// </summary>
		public bool ArrayInterchange { get { return (m_features & Features.ArrayInterchange) != 0; } set { m_features = value ? m_features | Features.ArrayInterchange : m_features & ~Features.ArrayInterchange; } }
		/// <summary>
		/// Allow the deserialize any the objects as a json string(System.String).
		/// The string will contain the JSON text of deserialized value.
		/// This flag is used for delayed deserialization when a value type is not known during deserialization. Default is true
		/// </summary>
		[Obsolete("This feature is obsolete. Use JsonString object when you need this functionality")]
		public bool StringInterchange { get { return (m_features & Features.StringInterchange) != 0; } set { m_features = value ? m_features | Features.StringInterchange : m_features & ~Features.StringInterchange; } }
		/// <summary>
		/// Allow to skip unknown object"s members during deserialization. If set to false, the exception will be thrown when an unknown member is met . Default is true
		/// </summary>
		public bool SkipUnknownMember { get { return (m_features & Features.SkipUnknownMember) != 0; } set { m_features = value ? m_features | Features.SkipUnknownMember : m_features & ~Features.SkipUnknownMember; } }
		/// <summary>
		/// Allow ignore properties unavailable for write access. If set to false, an exception will be thrown. Default is true
		/// </summary>
		public bool SkipUnaccessibleMembers { get { return (m_features & Features.SkipUnaccessibleMembers) != 0; } set { m_features = value ? m_features | Features.SkipUnaccessibleMembers : m_features & ~Features.SkipUnaccessibleMembers; } }
		/// <summary>
		/// Serialize/Deserialize enumeration as the value of the base type (integer). Default is false
		/// </summary>
		public bool SerializeEnumsAsInteger { get { return (m_features & Features.SerializeEnumsAsInteger) != 0; } set { m_features = value ? m_features | Features.SerializeEnumsAsInteger : m_features & ~Features.SerializeEnumsAsInteger; } }
		/// <summary>
		/// Allow serialization of an array of bytes as base64-encoded string. Default is true
		/// </summary>
		public bool SerializeByteArrayAsBase64 { get { return (m_features & Features.SerializeByteArrayAsBase64) != 0; } set { m_features = value ? m_features | Features.SerializeByteArrayAsBase64 : m_features & ~Features.SerializeByteArrayAsBase64; } }
		/// <summary>
		/// The maximum number of objects in graph(per WriteObject request). By default equals to <see cref="System.Int32.MaxValue"/>
		/// </summary>
		public ulong MaxItemsInObjectGraph { get; set; }
		/// <summary>
		/// List of available features
		/// </summary>
		public Features Features { get { return m_features; } set { m_features = value; } }
		/// <summary>
		/// Get data contract for this serializer
		/// </summary>
		public DataContract DataContract
		{
			get { return m_contract; }
		}
		/// <summary>
		/// Date and time format for serialization/deserialization of DateTime values Default to Sencha.Serialization.Json.DateTimeFormat
		/// </summary>
		/// <see cref="Sencha.Serialization.Json.DateTimeFormat"/>
		public string DateTimeFormat
		{
			get { return m_dateFormats[0]; }
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");
				m_dateFormats[0] = value;
			}
		}
		/// <summary>
		/// Format for serialization/deserialization of primitive values. Default to System.Globalization.CultureInfo.InvariantCulture
		/// </summary>
		public IFormatProvider Format { get { return m_format; } set { if (value == null) throw new ArgumentNullException("value"); m_format = value; } }
		/// <summary>
		/// Encdoing used for binary/text convetion. Default is UTF-8
		/// </summary>
		public Encoding Encoding
		{
			get { return m_encoding; }
#if STRICT
			private set
#else
			set
#endif
			{ if (value == null) throw new ArgumentNullException("value"); m_encoding = value; }
		}

		public JsonSerializer(Type serializableType)
			: this(DataContractCache.FetchOrCreate(serializableType), null)
		{

		}
		public JsonSerializer(Type serializableType, Type[] knownTypes)
			: this(DataContractCache.FetchOrCreate(serializableType), knownTypes)
		{

		}
		public JsonSerializer(DataContract contract)
			: this(contract, null)
		{

		}
		public JsonSerializer(DataContract contract, params Type[] knownTypes)
		{
			if (contract == null)
				throw new ArgumentNullException("contract");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			m_contract = contract;
			if (contract.Root != null)
				m_rootType = contract.Root.GetClrType();
			else
				m_rootType = typeof(object);

			m_dateFormats = new string[Json.DateTimeFormats.Length + 1];
			m_dateFormats[0] = Json.DateTimeFormats[0];
			Json.DateTimeFormats.CopyTo(m_dateFormats, 1);

			if (knownTypes != null)
			{
				for (int i = 0; i < knownTypes.Length; i++)
				{
					if (knownTypes[i] == null)
						continue;

					contract.AddKnownType(knownTypes[i]);
				}
			}

			this.Format = CultureInfo.InvariantCulture;
			this.Encoding = Json.DefaultEncoding;
			this.MaxItemsInObjectGraph = int.MaxValue;
			this.IgnoreCustomSerialization = false;
			this.ArrayInterchange = true;
			this.StringInterchange = true;
			this.SkipUnknownMember = true;
			this.SkipUnaccessibleMembers = true;
			this.SerializeEnumsAsInteger = false;
			this.SerializeByteArrayAsBase64 = true;

		}

		public override object ReadObject(Stream stream)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var jreader = new JsonStreamReader(stream, this.CreateSerializer());
			return this.ReadObject<object>(jreader);
		}
		public object ReadObject(TextReader reader)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			return this.ReadObject<object>(reader);
		}
		public T ReadObject<T>(Stream stream)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var jreader = new JsonStreamReader(stream, this.CreateSerializer());
			return this.ReadObject<T>(jreader);
		}
		public T ReadObject<T>(TextReader reader)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var jreader = new JsonTextReader(reader, this.CreateSerializer());
			return this.ReadObject<T>(jreader);
		}
		public T ReadObject<T>(JsonString jsonString)
		{
			if (jsonString == null)
				throw new ArgumentNullException("jsonString");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var jreader = jsonString.ToJsonReader(this.CreateSerializer());

			return this.ReadObject<T>(jreader);
		}
		public T ReadObject<T>(IJsonReader reader)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var serializer = reader.Context as AdHocSerializer;
			if (serializer == null)
				serializer = this.CreateSerializer();
			try
			{
				return serializer.ReadValue<T>(reader, typeof(T) == typeof(object) ? m_contract.Root : null);
			}
			catch (System.Threading.ThreadAbortException) { throw; }
			catch (System.OutOfMemoryException) { throw; }
			catch (System.StackOverflowException) { throw; }
			catch (System.Runtime.Serialization.SerializationException se) { ReflectionExtentions.TryToPreserveStackTrace(se); throw; }
			catch (Exception e) { throw new Exceptions.JsonSerializationException(e); }
			finally
			{
				serializer.Reset();
			}
		}
		public override void WriteObject(Stream stream, object graph)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			this.WriteObject<object>(stream, graph);
		}
		public void WriteObject(TextWriter writer, object graph)
		{
			this.WriteObject<object>(writer, graph);
		}
		public void WriteObject<T>(Stream stream, T graph)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var jwriter = new JsonStreamWriter(stream, this.CreateSerializer());
			this.WriteObject<T>(jwriter, graph);
		}
		public void WriteObject<T>(TextWriter writer, T graph)
		{
			if (writer == null)
				throw new ArgumentNullException("writer");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var jwriter = new JsonTextWriter(writer, this.CreateSerializer());
			this.WriteObject<T>(jwriter, graph);
		}
		public void WriteObject<T>(IJsonWriter writer, T graph)
		{
			if (writer == null)
				throw new ArgumentNullException("writer");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			var serializer = writer.Context as AdHocSerializer;
			if (serializer == null)
				serializer = this.CreateSerializer();

			try
			{
				serializer.WriteValue<T>(writer, graph, null);
				writer.Flush();
			}
			catch (System.Threading.ThreadAbortException) { throw; }
			catch (System.OutOfMemoryException) { throw; }
			catch (System.StackOverflowException) { throw; }
			catch (System.Runtime.Serialization.SerializationException se) { ReflectionExtentions.TryToPreserveStackTrace(se); throw; }
			catch (Exception e) { throw new Exceptions.JsonSerializationException(e); }
			finally
			{
				serializer.Reset();
			}
		}

		public ISerializationContext CreateContext()
		{
			return this.CreateSerializer();
		}
		private AdHocSerializer CreateSerializer()
		{
			var serializer = new AdHocSerializer
			{
				DataContract = m_contract,
				Features = m_features,
				Format = m_format,
				DateTimeFormats = m_dateFormats,
				Encoding = m_encoding,
				MaxItemsInObjectGraph = this.MaxItemsInObjectGraph
			};
			return serializer;
		}

		#region NotSupportedException
		public override bool IsStartObject(System.Xml.XmlDictionaryReader reader)
		{
			throw new NotSupportedException();
		}
		public override object ReadObject(System.Xml.XmlDictionaryReader m_reader, bool verifyObjectName)
		{
			throw new NotSupportedException();
		}
		public override void WriteEndObject(System.Xml.XmlDictionaryWriter writer)
		{
			throw new NotSupportedException();
		}
		public override void WriteObjectContent(System.Xml.XmlDictionaryWriter writer, object graph)
		{
			throw new NotSupportedException();
		}
		public override void WriteStartObject(System.Xml.XmlDictionaryWriter writer, object graph)
		{
			throw new NotSupportedException();
		}
		#endregion
	}
}
